SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
6 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
1.7 KB · 22 lines typescript
Raw Blame History
1import { NextRequest, NextResponse } from "next/server";2import { z } from "zod";3import { db } from "@/db/client";4import { listingMedia } from "@/db/schema";5import { assertSameOrigin, requireUser } from "@/lib/auth/session";6import { getListingFull, assertOwner } from "@/lib/listings/service";7import { countMedia, MAX_PER_LISTING, MAX_BYTES, newUploadToken } from "@/lib/listings/media";8import { apiError, handle, parseBody } from "@/lib/api";9export const dynamic = "force-dynamic";10const Body = z.object({ fileName: z.string().max(200).optional(), mimeType: z.string().max(80).optional(), bytes: z.number().int().positive().max(MAX_BYTES).optional() });11/** Équivalent local d'une URL pré-signée : jeton à usage unique, lié à l'annonce et au propriétaire ; l'upload se fait ensuite en PUT sur /api/uploads/<token>. */12export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {13  try {14    await assertSameOrigin(); const u = await requireUser(); const { id } = await params; const b = await parseBody(req, Body);15    const l = await getListingFull(id); if (!l) return apiError(404, "not_found", "Annonce introuvable"); assertOwner(l, u.id, u.roles);16    if ((await countMedia(id)) >= MAX_PER_LISTING) return apiError(422, "too_many", `Maximum ${MAX_PER_LISTING} photos par annonce`);17    const token = newUploadToken();18    const [m] = await db().insert(listingMedia).values({ listingId: id, storageKey: `pending/${token}`, mimeType: b.mimeType ?? null, position: l.media.length, status: "UPLOADING", uploadToken: token }).returning({ id: listingMedia.id });19    return NextResponse.json({ mediaId: m.id, uploadUrl: `/api/uploads/${token}`, method: "PUT", maxBytes: MAX_BYTES, expiresInSec: 900 });20  } catch (e) { return handle(e); }21}22